package com.google.sky.ai.simple;

/**
 * Evaluator for gomoku game state.
 *
 * @author sslautin
 * @version 1.0 18.11.2014
 */
public class Heuristic {

    // int representation of board positions
    public static final int EMPTY_P = 0;
    public static final int MINE_P = 1;
    public static final int NOT_MINE_P = 2;
    public static final int OUT_P = -1;

    // alpha-beta min and max values
    public static final int MIN_VALUE = Integer.MIN_VALUE;
    public static final int MAX_VALUE = Integer.MAX_VALUE;

    /**
     * Checks if game is over on this board (five-in-a-row present).
     */
    public static boolean isTerminalNode(final int[][] board) {

        final int score = calculateHeuristicValue(board);
        return score == MAX_VALUE || score == MIN_VALUE;
    }

    /**
     * Evaluation function.
     */
    public static int calculateHeuristicValue(final int[][] board) {

        int score = 0;
        int positionScore;

        for (int x = 0; x < board.length; x++) {
            for (int y = 0; y < board[x].length; y++) {
                final int position = board[x][y];
                if (position == MINE_P) {
                    positionScore = evaluatePosition(board, x, y);
                    if (positionScore == MAX_VALUE) {
                        return MAX_VALUE;
                    } else {
                        score += positionScore;
                    }
                } else if (position == NOT_MINE_P) {
                    positionScore = evaluatePosition(board, x, y);
                    if (positionScore == MAX_VALUE) {
                        return MIN_VALUE;
                    } else {
                        // pay attention on the other player's positions first
                        score -= 5 * positionScore;
                    }
                }
            }
        }

        return score;
    }

    /**
     * Calculates score for a given position.
     */
    private static int evaluatePosition(
        final int[][] board,
        final int x,
        final int y
    ) {

        int score = 0;

        final int s1 = scoreForSequences(board, x, y);
        if (s1 == MAX_VALUE) {
            return MAX_VALUE;
        } else {
            score += s1;
        }
        //score += scoreForCenterNear(board, x, y);
        //score += scoreForAnAllyNear(board, x, y);

        return score;
    }

    /**
     * Gets different scores for every stone sequence, for which the given position is start.
     */
    private static int scoreForSequences(
        final int[][] board,
        final int x,
        final int y
    ) {

        final int s1 = getSequenceForLine1(board, x, y).getScore();
        if (s1 == MAX_VALUE) {
            return MAX_VALUE;
        }
        final int s2 = getSequenceForLine2(board, x, y).getScore();
        if (s2 == MAX_VALUE) {
            return MAX_VALUE;
        }
        final int s3 = getSequenceForLine3(board, x, y).getScore();
        if (s3 == MAX_VALUE) {
            return MAX_VALUE;
        }
        final int s4 = getSequenceForLine4(board, x, y).getScore();
        if (s4 == MAX_VALUE) {
            return MAX_VALUE;
        }

        return s1 + s2 + s3 + s4;
    }

    /**
     * Returns a sequence (if any) on beam 1 (to upper-left).
     */
    private static Sequence getSequenceForLine1(
        final int[][] board,
        final int x,
        final int y
    ) {

        final int checked = board[x][y];
        final int beamStart = getPositionSafe(board, x + 1, y - 1);
        boolean open1;
        boolean open2 = false;
        int length = 1;

        if (beamStart == checked) {
            return Sequence.NONE;
        } else {
            open1 = (beamStart == EMPTY_P);
        }

        for (int i = x - 1, j = y + 1; i >= 0 && j < board[0].length; i--, j++) {
            final int beamPoint = getPositionSafe(board, i, j);

            if (beamPoint == checked) {
                length++;
            } else {
                open2 = (beamPoint == EMPTY_P);
                break;
            }
        }

        return getSequence(length, open1, open2);
    }

    /**
     * Returns a sequence (if any) on beam 2 (to up).
     */
    private static Sequence getSequenceForLine2(
        final int[][] board,
        final int x,
        final int y
    ) {

        final int checked = board[x][y];
        final int beamStart = getPositionSafe(board, x, y - 1);
        boolean open1;
        boolean open2 = false;
        int length = 1;

        if (beamStart == checked) {
            return Sequence.NONE;
        } else {
            open1 = (beamStart == EMPTY_P);
        }

        for (int j = y + 1; j < board[0].length; j++) {
            final int beamPoint = getPositionSafe(board, x, j);

            if (beamPoint == checked) {
                length++;
            } else {
                open2 = (beamPoint == EMPTY_P);
                break;
            }
        }

        return getSequence(length, open1, open2);
    }

    /**
     * Returns a sequence (if any) on beam 3 (to upper-right).
     */
    private static Sequence getSequenceForLine3(
        final int[][] board,
        final int x,
        final int y
    ) {

        final int checked = board[x][y];
        final int beamStart = getPositionSafe(board, x - 1, y - 1);
        boolean open1;
        boolean open2 = false;
        int length = 1;

        if (beamStart == checked) {
            return Sequence.NONE;
        } else {
            open1 = (beamStart == EMPTY_P);
        }

        for (int i = x + 1, j = y + 1; i < board.length && j < board[0].length; i++, j++) {
            final int beamPoint = getPositionSafe(board, i, j);

            if (beamPoint == checked) {
                length++;
            } else {
                open2 = (beamPoint == EMPTY_P);
                break;
            }
        }

        return getSequence(length, open1, open2);
    }

    /**
     * Returns a sequence (if any) on beam 4 (to right).
     */
    private static Sequence getSequenceForLine4(
        final int[][] board,
        final int x,
        final int y
    ) {

        final int checked = board[x][y];
        final int beamStart = getPositionSafe(board, x - 1, y);
        boolean open1;
        boolean open2 = false;
        int length = 1;

        if (beamStart == checked) {
            return Sequence.NONE;
        } else {
            open1 = (beamStart == EMPTY_P);
        }

        for (int i = x + 1; i < board.length; i++) {
            final int beamPoint = getPositionSafe(board, i, y);

            if (beamPoint == checked) {
                length++;
            } else {
                open2 = (beamPoint == EMPTY_P);
                break;
            }
        }

        return getSequence(length, open1, open2);
    }

    /**
     * Returns corresponding sequence, if any.
     */
    private static Sequence getSequence(final int length, final boolean open1, final boolean open2) {

        if (length >= 5) {
            return Sequence.FIVE_IN_A_ROW;
        } else if ((!open1 && !open2) || (length < 2)) {
            return Sequence.NONE;
        } else {
            final boolean open = (open1 && open2);
            Sequence sequence = Sequence.NONE;

            switch (length) {
                case 2:
                    if (open) {
                        sequence = Sequence.OPEN_TWO;
                    } else {
                        sequence = Sequence.HALF_TWO;
                    }
                    break;

                case 3:
                    if (open) {
                        sequence = Sequence.OPEN_THREE;
                    } else {
                        sequence = Sequence.HALF_THREE;
                    }
                    break;

                case 4:
                    if (open) {
                        sequence = Sequence.OPEN_FOUR;
                    } else {
                        sequence = Sequence.HALF_FOUR;
                    }
                    break;

                default:
                    break;
            }

            return sequence;
        }
    }

    /**
     * Gets 1 point for an ally near.
     */
    private static int scoreForAnAllyNear(
        final int[][] board,
        final int x,
        final int y
    ) {

        if (isAllyStonesNear(board, x, y)) {
            return 1;
        }
        return 0;
    }

    /**
     * Gets the bigger value, the more near position to the center.
     */
    private static int scoreForCenterNear(
        final int[][] board,
        final int x,
        final int y
    ) {

        int score = 0;

        final int width = board.length;
        final int height = board[0].length;

        if (x < (width / 2)) {
            score += x;
        } else {
            score += width - 1 - x;
        }

        if (y < (height / 2)) {
            score += y;
        } else {
            score += height - 1 - y;
        }

        return score;
    }

    /**
     * Returns true, if any ally stone near (3x3 field).
     */
    private static boolean isAllyStonesNear(
        final int[][] board,
        final int x,
        final int y
    ) {

        final int checked = board[x][y];

        for (int i = x - 1; i <= x + 1; i++) {
            for (int j = y - 1; j <= y + 1; j++) {
                if (i == x && j == y) {
                    continue;
                }
                if (getPositionSafe(board, i, j) == checked) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Checks for bounds before get int from array. Returns {@link Heuristic#OUT_P} if out of bounds.
     */
    private static int getPositionSafe(
        final int[][] board,
        final int x,
        final int y
    ) {

        if (x < 0 || y < 0 || x >= board.length || y >= board[0].length) {
            return OUT_P;
        } else {
            return board[x][y];
        }
    }
}
